﻿using NullTodoList.Properties;
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Windows.Forms;
using System.Net;
using System.Net.Mail;
using System.Media;
using System.Text;
using System.Reflection;
using System.Drawing.Drawing2D;

namespace NullTodoList
{
    /// <summary>
    /// Главная форма.
    /// </summary>
    public partial class frmMain : Form
    {
        private const Int32 ALL_TAGS = -1;
        private const Int32 COLOR_TAG_BORDER = 0xB3CEE1;
        private const Int32 COLOR_TAG_FILL = 0xE0EAF1;

        // Кэшированный список тегов.
        private Dictionary<Int32, String> tags;
        // Активный тег.
        private Int32 filterTag;
        // Настройки.
        private Settings settings;

        private List<List<Segment>> levels;

        #region ctor
        /// <summary>
        /// Конструктор по-умолчанию.
        /// </summary>
        public frmMain()
        {
            InitializeComponent();

            tags = new Dictionary<Int32, String>();

            filterTag = ALL_TAGS;

            timerNotify.Tag = new List<Int32>();

            settings = Settings.LoadFromFile();

            levels = new List<List<Segment>>();
            
            typeof(Control).GetProperty(
                "DoubleBuffered",
                BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.SetProperty
                ).SetValue(pnlTags, true, null);
        }    
        #endregion

        #region Static methods
        /// <summary>
        /// Создание полностью непрозрачного цвета (alpha=255) из Int32.
        /// </summary>
        /// <param name="value"></param>
        /// <returns></returns>
        public static Color MakeColor(Int32 value)
        {
            return Color.FromArgb(0xFF, value >> 16 & 0xFF, value >> 8 & 0xFF, value & 0xFF);
        }
        #endregion

        #region Private methods
        #region frmMain events
        private void frmMain_Load(object sender, System.EventArgs e)
        {
            DoUpdate();
        }
        #endregion

        #region listBoxView events
        /// <summary>
        /// Расчет высоты строки.
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void listBoxView_MeasureItem(object sender, MeasureItemEventArgs e)
        {
            e.ItemHeight = 50;
        }

        /// <summary>
        /// Отрисовка строки.
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void listBoxView_DrawItem(object sender, DrawItemEventArgs e)
        {
            try
            {
                var listBox = sender as CustomDrawList;
                if (listBox == null)
                    return;
                TodoItem item = listBox.Items[e.Index] as TodoItem;
                DrawTask(e.Graphics, e.Bounds, item, e.Index % 2 != 0);
            }
            catch (ArgumentOutOfRangeException)
            {
            }
        }

        /// <summary>
        /// Отслеживание перетаскивания строки на панель тегов, редактирование при двойном клике
        /// и открытие контекстного меню.
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void listBoxView_MouseDown(object sender, MouseEventArgs e)
        {
            var listBox = sender as CustomDrawList;
            if (listBox == null)
                return;
            // Получаем задание, по которому пользователь кликнул мышкой.
            TodoItem item = null;
            var index = listBox.IndexFromPoint(e.Location);
            try
            {
                item = listBox.Items[index] as TodoItem;
            }
            catch (ArgumentOutOfRangeException)
            {
            }

            switch (e.Button)
            {
                case MouseButtons.Left:
                    if (item == null)
                        return;
                    // Для двойного клика вызываем редактор задачи,
                    // для одинарного начинаем процесс перетаскивания.
                    if (e.Clicks > 1)
                        EditItem(item);
                    else
                        DoDragDrop(item, DragDropEffects.Move);
                    break;
                case MouseButtons.Right:
                    // Вызываем контекстное меню.
                    listContextMenu.Tag = item;
                    toolItemChange.Enabled = item != null;
                    toolItemDelete.Enabled = item != null;
                    listContextMenu.Show(Cursor.Position);
                    break;
            }
        }

        /// <summary>
        /// Метод возвращает иконку, ассоциированную с данным типом оповещения.
        /// </summary>
        /// <param name="notify"></param>
        /// <returns></returns>
        private Image GetNotifyImage(NotifyKind notify)
        {
            Image img = null;
            switch (notify)
            {
                case NotifyKind.Alert:
                    img = Resources.alarm;
                    break;
                case NotifyKind.SMS:
                    img = Resources.sms;
                    break;
                case NotifyKind.Mail:
                    img = Resources.mail;
                    break;
            }
            return img;
        }

        /// <summary>
        /// Отрисовка задания в списке.
        /// </summary>
        /// <param name="g">Холст, на котором отрисовывается задание.</param>
        /// <param name="rect">Ограничивающий прямоугольник.</param>
        /// <param name="item">Задание, которое будет отрисовано.</param>
        /// <param name="isOdd">Является ли строка нечетной.</param>
        private void DrawTask(Graphics g, Rectangle rect, TodoItem item, Boolean isOdd)
        {
            if (item == null)
                return;
            // Закрашиваем фон у четных строк.
            SolidBrush brush;
            if (isOdd)
                brush = new SolidBrush(Color.White);
            else
                brush = new SolidBrush(Color.WhiteSmoke);
            g.FillRectangle(brush, rect);
            // Координаты первой строки.
            var lineRect = rect;
            lineRect.Inflate(-6, -2);
            lineRect.Height = lineRect.Height / 3;
            // Набор флагов для отрисовки текста.
            var flags = TextFormatFlags.Left | TextFormatFlags.Top | TextFormatFlags.EndEllipsis | TextFormatFlags.SingleLine | TextFormatFlags.NoClipping;
            // Рисуем время.
            var tempRect = lineRect;
            var str = item.DateBegin.ToString("dd MMMM yyyy HH:mm");
            // Для процесса показываем время окончания.
            if (item.Category == TodoItem.CATEGORY_PROCESS)
            {
                var endTime = item.DateBegin.TimeOfDay + TimeSpan.FromMinutes(item.Length);
                str += "-" + endTime.ToString(@"hh\:mm");
            }
            var size = TextRenderer.MeasureText(g, str, Font);
            TextRenderer.DrawText(g, str, Font, tempRect, Color.Black, flags | TextFormatFlags.Right);
            tempRect.Width -= size.Width;
            // Рисуем иконки оповещения.
            NotifyKind notify;
            for (var i = 1; i <= 4; i *= 2)
            {
                notify = (NotifyKind)i;
                if ((item.Notify & notify) != 0)
                {
                    var img = GetNotifyImage(notify);
                    if (img != null)
                    {
                        tempRect.Width -= img.Size.Width + 4;
                        g.DrawImage(img, tempRect.Right, tempRect.Y);
                    }
                }
            }
            // Рисуем иконку приоритета.
            if (item.Prioritet >= 0 && item.Prioritet < imageListNumbers.Images.Count)
            {
                imageListNumbers.Draw(g, tempRect.Left, tempRect.Y, item.Prioritet);
                tempRect.X += imageListNumbers.ImageSize.Width;
                tempRect.Width -= imageListNumbers.ImageSize.Width;
            }
            // Рисуем заголовок.
            switch (item.Category)
            {
                
                case 1:
                    str = "Дело";
                    break;
                case 2:
                    str = "Процесс";
                    break;
                case 4:
                    str = "Заметка";
                    break;
                default:
                    str = String.Empty;
                    break;
            }
            str = str + ": " + item.Text; 
            var fontBold = new Font(Font, item.IsComplete ? FontStyle.Bold | FontStyle.Strikeout : FontStyle.Bold);
            TextRenderer.DrawText(g, str, fontBold, tempRect, Color.Black, flags);
            lineRect.Y += lineRect.Height;
            // Рисуем комментарий.
            str = item.Comment;
            var fontItalic = new Font(Font, FontStyle.Italic);
            TextRenderer.DrawText(g, str, Font, lineRect, Color.DarkGray, flags);
            lineRect.Y += lineRect.Height;
            // Рисуем иконку тега.
            g.DrawImage(Resources.tags, lineRect.Location);
            lineRect.X += Resources.tag.Size.Width + 6;
            // Отрисовываем теги.
            var pen = new Pen(MakeColor(COLOR_TAG_BORDER));
            brush.Color = MakeColor(COLOR_TAG_FILL);
            flags |= TextFormatFlags.HorizontalCenter | TextFormatFlags.VerticalCenter;
            foreach (var id in item.Tags)
            {
                var tag = tags[id];
                size = TextRenderer.MeasureText(g, tag, Font);
                lineRect.Width = size.Width + 10;
                if (lineRect.Right > rect.Right)
                    break;
                g.FillRectangle(brush, lineRect);
                g.DrawRectangle(pen, lineRect);
                TextRenderer.DrawText(g, tag, Font, lineRect, Color.Black, flags);
                lineRect.X += lineRect.Width + 4;
            }
            // Линия, разделяющая строки.
            pen.Color = Color.Silver;
            g.DrawLine(pen, rect.Left + 2, rect.Bottom - 1, rect.Right - 2, rect.Bottom - 1);
        }
        #endregion

        #region toolControls events
        /// <summary>
        /// Добавление новой записи.
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void toolBtnAdd_Click(object sender, EventArgs e)
        {
            AddItem();
        }

        /// <summary>
        /// Выбор отображаемых категорий.
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void toolItemTask_CheckedChanged(object sender, EventArgs e)
        {
            LoadToDoItems();
        }

        private void toolBtnSettings_Click(object sender, EventArgs e)
        {
            var form = new frmSettings(settings);

            if (form.ShowDialog() == DialogResult.OK)
                settings.SaveToFile();
        }
        #endregion

        #region Tags
        /// <summary>
        /// Добавление тега на панель тегов.
        /// </summary>
        /// <param name="text">Текст тега.</param>
        /// <param name="id">ID тега.</param>
        /// <param name="fontSize">Размер шрифта.</param>
        /// <param name="list">Список, куда будет добавлен контрол-тэг.</param>
        private void AddTag(String text, Int32 id, Int32 fontSize, List<Control> list)
        {
            var label = new Label();
            label.AutoSize = true;
            label.Text = text;
            label.Tag = id;
            label.Font = new Font(Font.FontFamily, fontSize);
            label.Cursor = Cursors.Hand;
            label.AllowDrop = id != ALL_TAGS;
            if (id == filterTag)
                label.BackColor = MakeColor(COLOR_TAG_BORDER);
            label.Click += (sender, e) =>
            {
                // Получаем лейбл, для которого сработало событие.
                var lb = (sender as Label);
                if (lb == null)
                    return;
                // Цвет неактивного тега.
                var color = Color.FromKnownColor(KnownColor.Control);
                var activeColor = MakeColor(COLOR_TAG_BORDER);
                // Отмечаем цветом теги.
                foreach (Control control in lb.Parent.Controls)
                    if (control == lb)
                        control.BackColor = activeColor;
                    else
                        control.BackColor = color;
                // Запоминаем ИД тега.
                filterTag = (Int32)lb.Tag;
                // Перезагружаем задания в соотвествии с фильтром.
                LoadToDoItems();
            };
            label.DragEnter += (sender, e) =>
            {
                if (e.Data.GetDataPresent(typeof(TodoItem)))
                    e.Effect = DragDropEffects.Move;
                else
                    e.Effect = DragDropEffects.None;
            };
            label.DragDrop += (sender, e) =>
            {
                // Получаем задачу, которую перетащил юзер.
                var item = e.Data.GetData(typeof(TodoItem)) as TodoItem;
                // Тег, на который перетащили задачу.
                var lb = (sender as Label);
                if (item == null || lb == null)
                    return;
                // Получаем ИД тега.
                var tagID = (Int32)lb.Tag;
                try
                {
                    // Добавляем запись в БД.
                    itemTagsTableAdapter.Insert(item.ID, tagID);
                    // Загрузить теги.
                    LoadTags();
                    // Перезагружаем задания в соотвествии с фильтром.
                    LoadToDoItems();
                }
                catch
                {
                }
            };
            list.Add(label);
        }

        /// <summary>
        /// Загрузка доступных тегов из БД и отображение тегов, присутствующих в текущих записях на панели.
        /// </summary>
        private void LoadTags()
        {
            // Очищаем кэшированный список тегов.
            tags.Clear();
            // Создаем временный список для меток-тегов.
            var list = new List<Control>();
            // Добавляем общий тег.
            AddTag("Все", ALL_TAGS, 20, list);
            // Максимальный вес тега.
            var temp = tagsViewTableAdapter.GetMaxTagWeight();
            var maxWeight = temp.GetValueOrDefault(1);
            // Получаем теги которые присутствуют в текущих записях.
            var rows = tagsViewTableAdapter.GetData();
            foreach (var row in rows)
            {
                //Кэшируем тег.
                tags.Add(row.ID, row.Name);
                // Добавляем метку-тег.
                AddTag(row.Name, row.ID, 10 + (10 * row.Weight / maxWeight), list);
            }
            // Подготавливаем панель тегов.
            pnlTags.SuspendLayout();
            // Удаляем все дочерние контролы.
            pnlTags.Controls.Clear();
            // Добавляем новые контролы.
            pnlTags.Controls.AddRange(list.ToArray());
            pnlTags.ResumeLayout(false);
            pnlTags.PerformLayout();
        }
        #endregion

        #region Notification methods
        private void NotifyAlert(TodoItem item)
        {
            if (settings.Alert.PlaySound)
            {
                var player = new SoundPlayer();
                player.SoundLocation = settings.Alert.SoundFile;
                player.Play();
            }
            MessageBox.Show(item.Text, "Напоминание", MessageBoxButtons.OK, MessageBoxIcon.Information,
                MessageBoxDefaultButton.Button1, MessageBoxOptions.ServiceNotification);
        }

        private void NotifySMS(TodoItem item)
        {
            try
            {
                var smsc = new SMSC();
                smsc.SMSC_LOGIN = settings.SMS.Login;
                smsc.SMSC_PASSWORD = settings.SMS.Password;
                smsc.send_sms(settings.SMS.Phone, item.Text, settings.SMS.Translit ? 1 : 0);
                // TODO: возможно сделать разбор ответа.
            }
            catch
            {
                // TODO: залогировать исключение.
            }
        }

        private void NotifyMail(TodoItem item)
        {
            try
            {
                // 
                var client = new SmtpClient(settings.Mail.SmtpHost, settings.Mail.SmtpPort);
                // Использовать ли SSL.
                client.EnableSsl = settings.Mail.EnableSsl;
                // Данные авторизации.
                client.Credentials = new NetworkCredential(settings.Mail.From, settings.Mail.Password);
                // Создаем адрес "От кого".
                var from = new MailAddress(settings.Mail.From, settings.Mail.From, Encoding.UTF8);
                // Создаем адресс "Кому".
                var to = new MailAddress(settings.Mail.To);
                // Создаем сообщение.
                var message = new MailMessage(from, to);
                message.Body = String.Format("{0}\r\n\r\n{1}", item.Text, item.Comment);
                message.BodyEncoding = System.Text.Encoding.UTF8;
                message.Subject = "NullTodoList напоминание";
                message.SubjectEncoding = Encoding.UTF8;
                // Отслеживание состояния отправки.
                //client.SendCompleted += (sender, e) =>
                //    {
                //    }
                // Отправка письма.
                client.SendAsync(message, null);
            }
            catch
            {
                // TODO: залогировать исключение.
            }
        }
        #endregion

        #region Context menu list     
        /// <summary>
        /// Редактирование задачи, с помощью контектстного меню.
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void toolItemChange_Click(object sender, EventArgs e)
        {
            var item = listContextMenu.Tag as TodoItem;
            if (item != null)
                EditItem(item);
        }

        /// <summary>
        /// Удаление задачи, с помощью контектстного меню.
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void toolItemDelete_Click(object sender, EventArgs e)
        {
            var item = listContextMenu.Tag as TodoItem;
            if (item != null && MessageBox.Show(String.Format(Resources.MSG_DELETE_ITEM, item),
                Text, MessageBoxButtons.YesNo, MessageBoxIcon.Question) == DialogResult.Yes)
            {
                itemsTableAdapter.Delete(item.ID);
                DoUpdate();
            }
        }
        #endregion

        #region Others
        /// <summary>
        /// Полное обновление.
        /// </summary>
        private void DoUpdate()
        {
            // Загрузка тэгов.
            LoadTags();
            // Загрузка заданий.
            LoadToDoItems();
            // Установка таймера оповещений.
            SetupTimer();
        }
        
        /// <summary>
        /// Загрузить задачи в текущий просмотрщик.
        /// </summary>
        private void LoadToDoItems()
        {
            switch (tabViews.SelectedIndex)
            {
                case 0:
                    ReloadToDoItemsToList();
                    break;
                case 1:
                    ReloadToDoItemsToCalendar();
                    break;
                case 2:
                    ReloadToDoItemsToAnalogClock();
                    break;
            }
        }

        /// <summary>
        /// Загрузка задач в список.
        /// </summary>
        private void ReloadToDoItemsToList()
        {
            listBoxView.BeginUpdate();
            try
            {
                var scroll = listBoxView.AutoScrollPosition;
                // Очищаем список.
                listBoxView.Items.Clear();
                todoListDataSet.itemsDataTable rows;
                rows = itemsTableAdapter.GetDataByFilters(GetCategoryFilter(), filterTag, GetTextFilter());
                foreach (var row in rows)
                {
                    // Кэшируем запись.
                    var item = TodoItem.FromRow(row);
                    // Получаем список ИД тегов для текущей записи.
                    item.RequestTags += (sender, e) =>
                    {
                        var tags = itemTagsTableAdapter.GetTagsIDByItemID(row.id);
                        foreach (var tag in tags)
                            e.TagList.Add(tag.tagID);
                    };
                    // Добавляем запись в список.
                    listBoxView.Items.Add(item);
                }
                listBoxView.AutoScrollPosition = new Point(Math.Abs(scroll.X), Math.Abs(scroll.Y));
            }
            finally
            {
                listBoxView.EndUpdate();
            }
        }

        /// <summary>
        /// Загрузка задач в календарь.
        /// </summary>
        private void ReloadToDoItemsToCalendar()
        {
            listBoxCalendar.BeginUpdate();
            try
            {
                var scroll = listBoxCalendar.AutoScrollPosition;
                // Очищаем список.
                listBoxCalendar.Items.Clear();
                todoListDataSet.itemsDataTable rows;
                switch (dateTimeView.ViewMode)
                {
                    case DateTimeView.VIEW_MODE_HOURS:
                        rows = itemsTableAdapter.GetDataByFiltersAndHour(GetCategoryFilter(), filterTag, GetTextFilter(), dateTimeView.FilterDateTime);
                        break;
                    case DateTimeView.VIEW_MODE_DAYS:
                        rows = itemsTableAdapter.GetDataByFiltersAndDay(GetCategoryFilter(), filterTag, GetTextFilter(), dateTimeView.FilterDateTime);
                        break;
                    default: // DateTimeView.VIEW_MODE_MONTHS:
                        rows = itemsTableAdapter.GetDataByFiltersAndMonth(GetCategoryFilter(), filterTag, GetTextFilter(), dateTimeView.FilterDateTime);
                        break;
                }
                foreach (var row in rows)
                {
                    // Кэшируем запись.
                    var item = TodoItem.FromRow(row);
                    // Получаем список ИД тегов для текущей записи.
                    item.RequestTags += (sender, e) =>
                    {
                        var tags = itemTagsTableAdapter.GetTagsIDByItemID(row.id);
                        foreach (var tag in tags)
                            e.TagList.Add(tag.tagID);
                    };
                    // Добавляем запись в список.
                    listBoxCalendar.Items.Add(item);
                }
                listBoxCalendar.AutoScrollPosition = new Point(Math.Abs(scroll.X), Math.Abs(scroll.Y));
            }
            finally
            {
                listBoxCalendar.EndUpdate();
            }
        }

        /// <summary>
        /// Загрузка задач в "часы".
        /// </summary>
        private void ReloadToDoItemsToAnalogClock()
        {
            // Очищаем список.
            levels.Clear();
            todoListDataSet.itemsDataTable rows;
            rows = itemsTableAdapter.GetDataByFiltersAndDay(GetCategoryFilter(), filterTag, GetTextFilter(), DateTime.Now);
            foreach (var row in rows)
            {
                // Кэшируем запись.
                var item = TodoItem.FromRow(row);
                // Получаем список ИД тегов для текущей записи.
                //item.RequestTags += (sender, e) =>
                //{
                //    var tags = itemTagsTableAdapter.GetTagsIDByItemID(row.id);
                //    foreach (var tag in tags)
                //        e.TagList.Add(tag.tagID);
                //};
                // Добавляем запись.
                AddItemToAnalogClock(item, 0);
            }
        }

        /// <summary>
        /// Текущий фильтр категорий.
        /// </summary>
        /// <returns>Числовое значение фильтра.</returns>
        private Int32 GetCategoryFilter()
        {
            var filter = 0;
            if (toolItemTask.Checked)
                filter |= TodoItem.CATEGORY_TASK;
            if (toolItemProcess.Checked)
                filter |= TodoItem.CATEGORY_PROCESS;
            if (toolItemReminder.Checked)
                filter |= TodoItem.CATEGORY_REMINDER;
            return filter;
        }

        /// <summary>
        /// Текущий тексттовый фильтр.
        /// </summary>
        /// <returns></returns>
        private String GetTextFilter()
        {
            var filter = edtSearchText.Text;
            return String.IsNullOrEmpty(filter) ? "%" : "%" + filter + "%";
        }

        /// <summary>
        /// Установка таймера на оповещение следюущего события.
        /// </summary>
        private void SetupTimer()
        {
            // Получаем список ID задач, для которых были назначены уведомления.
            var list = timerNotify.Tag as List<Int32>;
            if (list == null)
                return;
            // Очищаем список.
            list.Clear();
            // Получаем ближайщую задачу по дате.
            var nextDate = itemsTableAdapter.GetNextNotify(DateTime.Now);
            if (!nextDate.HasValue)
                return;
            // Получаем список ID, для которых в ближайшее время должно сработать напоминание
            // и сохраняем их в список.
            var rows = notifyIDsViewTableAdapter.GetData(nextDate.Value); 
            foreach (var row in rows)
                list.Add(row.id);
            // Получаем разницу в мсек между текущей датой и полученной.
            Int32 diff;
            try
            {
                diff = checked((Int32)(nextDate.Value - DateTime.Now).TotalMilliseconds);
            }
            catch (OverflowException)
            {
                // Время ожидания превышает значение MaxInt.
                // Устанавливаем таймер на максимально возможное время.
                diff = Int32.MaxValue;
                // Очищаем список ID, т.к. следующее время срабатывания таймера
                // не является временем оповещения для данных ID.
                list.Clear();
            }
            // Устанавливаем таймер.
            timerNotify.Interval = diff;
            timerNotify.Enabled = true;
        }

        /// <summary>
        /// Оповещение о задачах.
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void timerNotify_Tick(object sender, EventArgs e)
        {
            timerNotify.Enabled = false;
            // Получаем список ID задач, для которых были назначены уведомления.
            var list = timerNotify.Tag as List<Int32>;
            if (list == null)
                return;
            foreach (var id in list)
            {
                // Запрашиваем из БД задание по сохраненному ранее ID.
                var rows = itemsTableAdapter.GetItemByID(id);
                if (rows.Count == 0)
                    continue;
                // Преобразует его в объект TodoItem.
                var item = TodoItem.FromRow(rows[0]);
                // Вызываем установленные оповещения.
                if ((item.Notify & NotifyKind.Alert) != 0)
                    NotifyAlert(item);
                if ((item.Notify & NotifyKind.SMS) != 0)
                    NotifySMS(item);
                if ((item.Notify & NotifyKind.Mail) != 0)
                    NotifyMail(item);
            }
            // Заводим таймер снова.
            SetupTimer();
        }

        /// <summary>
        /// Добавление нового задания.
        /// </summary>
        private void AddItem()
        {
            var form = new frmAdding(null);

            if (form.ShowDialog() == DialogResult.OK)
            {
                var item = form.Item;
                // Добавляем новую запись в базу.
                itemsTableAdapter.Insert(
                    item.Category,
                    item.Text,
                    item.Comment,
                    item.Prioritet,
                    item.IsComplete,
                    item.DateBegin,
                    (Int32)item.Notify,
                    TodoItem.INVALID_ITEM,
                    item.Parent,
                    item.Length
                 );
                // Получаем ИД добавленной записи.
                var id = itemsTableAdapter.GetLastID();
                // Добавляем теги к добавленной записи.
                foreach (var obj in form.lbxTags.CheckedItems)
                {
                    var tag = obj as Tag;
                    if (tag != null)
                        itemTagsTableAdapter.Insert(id, tag.ID);
                }
                DoUpdate();
            }
        }

        /// <summary>
        /// Изменение задания.
        /// </summary>
        /// <param name="item"></param>
        private void EditItem(TodoItem item)
        {
            var form = new frmAdding(item);

            if (form.ShowDialog() == DialogResult.OK)
            {
                item = form.Item;
                // Пользователь отмечает задание как выполненное.
                // Проверяем, не входит ли задание в цепочку заданий.
                // Если входит, то проверяем, выполнено ли родительское задание.
                if (item.IsComplete && item.Parent != TodoItem.INVALID_ITEM)
                {

                    // Запрашиваем из БД родительское задание.
                    var rows = itemsTableAdapter.GetItemByID(item.Parent);
                    if (rows.Count != 0)
                    {
                        // Преобразует его в объект TodoItem.
                        var parent = TodoItem.FromRow(rows[0]);
                        // Родительское задание не выполнено.
                        if (!parent.IsComplete)
                        {
                            item.IsComplete = false;
                            if (MessageBox.Show(String.Format(Resources.MSG_PARENT_NOT_COMPLITED, parent.Text), 
                                Text, MessageBoxButtons.YesNo, MessageBoxIcon.Question) != DialogResult.Yes)
                                return;
                        }
                    }
                }

                // Добавляем новую запись в базу.
                itemsTableAdapter.Update(
                    item.Category,
                    item.Text,
                    item.Comment,
                    item.Prioritet,
                    item.IsComplete,
                    item.DateBegin,
                    (Int32)item.Notify,
                    TodoItem.INVALID_ITEM,
                    item.Parent,
                    item.Length,
                    item.ID
                 );
                // Актуализируем теги.
                // Удаляем все теги, которые связаны с этим заданием.
                itemTagsTableAdapter.DeleteByItemID(item.ID);
                // Добавляем указанные теги.
                foreach (var obj in form.lbxTags.CheckedItems)
                {
                    var tag = obj as Tag;
                    if (tag != null)
                        itemTagsTableAdapter.Insert(item.ID, tag.ID);
                }
                DoUpdate();
            }
        }

        private void edtAddTodoItem_KeyPress(object sender, KeyPressEventArgs e)
        {
            if (e.KeyChar == '\r')
            {
                btnAddTextTodo_Click(sender, e);
                e.Handled = true;
            }
        }

        private void btnAddTextTodo_Click(object sender, EventArgs e)
        {
            var item = TodoItem.FromString(edtAddTodoItem.Text);
            // Добавляем новую запись в базу.
            itemsTableAdapter.Insert(
                item.Category,
                item.Text,
                item.Comment,
                item.Prioritet,
                item.IsComplete,
                item.DateBegin,
                (Int32)item.Notify,
                TodoItem.INVALID_ITEM,
                item.Parent,
                item.Length
                );
            LoadToDoItems();
        }

        private void edtSearchText_TextChanged(object sender, EventArgs e)
        {
            LoadToDoItems();
        }

        /// <summary>
        /// Изменение режима отображения или даты календаря.
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void dateTimeView_DateChanged(object sender, EventArgs e)
        {
            todoListDataSet.countersViewDataTable rows;
            switch (dateTimeView.ViewMode)
            {
                case DateTimeView.VIEW_MODE_HOURS:
                    rows = countersViewTableAdapter.GetHourCounters(dateTimeView.FilterDateTime);
                    break;
                case DateTimeView.VIEW_MODE_DAYS:
                    rows = countersViewTableAdapter.GetDayCounters(dateTimeView.FilterDateTime);
                    break;
                default: // DateTimeView.VIEW_MODE_MONTHS:
                    rows = countersViewTableAdapter.GetMonthCounters(dateTimeView.FilterDateTime);
                    break;
            }
            //
            foreach (var row in rows)
                dateTimeView.SetCounter(row.ID, row.Count);
            LoadToDoItems();
        }

        /// <summary>
        /// Изменение режима просмотра.
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void tabViews_SelectedIndexChanged(object sender, EventArgs e)
        {
            timerAnalogClock.Enabled = false;
            switch (tabViews.SelectedIndex)
            {
                case 0:
                    LoadToDoItems();
                    break;
                case 1:
                    dateTimeView.ViewMode = dateTimeView.ViewMode;
                    break;
                case 2:
                    LoadToDoItems();
                    timerAnalogClock.Enabled = true;
                    break;
            }
        }
        #endregion

        #region AnalogClock
        /// <summary>
        /// Отображение текущего времени в аналоговых часах.
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void timerAnalogClock_Tick(object sender, EventArgs e)
        {
            analogClock.Time = DateTime.Now.TimeOfDay;
        }

        /// <summary>
        /// Добавление задания в "часы".
        /// </summary>
        /// <param name="item"></param>
        /// <param name="index"></param>
        private void AddItemToAnalogClock(TodoItem item, Int32 index)
        {
            var begin = AnalogClock.HourToAngle(item.DateBegin.TimeOfDay) - 90;
            var end = (360f / 12f) * (item.Length / 60f);
            var segment = new Segment(begin, end == 0 ? 0.1f : end);
            AddItemToAnalogClock(segment, index);
        }

        /// <summary>
        /// Добавление сегмента в "часы".
        /// </summary>
        /// <param name="segment"></param>
        /// <param name="index"></param>
        private void AddItemToAnalogClock(Segment segment, Int32 index)
        {
            if (index == levels.Count)
                levels.Add(new List<Segment>());
            var level = levels[index];
            if (level.Exists(seg => seg.IsIntersect(segment)))
                AddItemToAnalogClock(segment, index + 1);
            else
                level.Add(segment);
        }

        /// <summary>
        /// Ограничивающий прямоугольник для указанного уровня.
        /// </summary>
        /// <param name="level"></param>
        /// <param name="size"></param>
        /// <returns></returns>
        private RectangleF CreateLevelBounds(Int32 level, Single size)
        {
            var radius = AnalogClock.RADIUS_SIZE + level * size;
            return new RectangleF(-radius, -radius, radius * 2, radius * 2);
        }

        /// <summary>
        /// Получение цвета по индексу.
        /// </summary>
        /// <param name="index"></param>
        /// <returns></returns>
        private Color GetColor(Int32 index)
        {
            var colors = new Color[] { Color.Red, Color.Blue, Color.Green, Color.Yellow,
                Color.Brown, Color.Pink, Color.SkyBlue, Color.Orange,
                Color.Tomato, Color.Navy, Color.Lime, Color.Violet };

            return colors[index % colors.Length];
        }

        /// <summary>
        /// Отрисовка сегментов на аналоговых часах.
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void analogClock_Paint(object sender, PaintEventArgs e)
        {
            var pen = new Pen(Color.Red, 8);
            // Начало пера закругленный.
            pen.StartCap = LineCap.Round;
            // Конец пера закругленный.
            pen.EndCap = LineCap.Round;
            // Выравнивание пера посередине.
            pen.Alignment = PenAlignment.Outset;

            for (var i = 0; i < levels.Count; i++)
            {
                var level = levels[i];
                var rect = CreateLevelBounds(i + 1, pen.Width);
                for (var j = 0; j < level.Count; j++)
                {
                    var segment = level[j];
                    pen.Color = GetColor(j);
                    e.Graphics.DrawArc(pen, rect, segment.begin, segment.length);
                }
            }
        }
        #endregion
        #endregion

        #region Public methods
        #endregion
    }

    struct Segment
    {
        public Single begin;
        public Single length;

        public Segment(Single begin, Single length)
        {
            this.begin = begin;
            this.length = length;
        }

        private Single end
        {
            get { return length == 0.1 ? begin : begin + length; }
        }

        public Boolean IsIntersect(Segment segment)
        {
            return (begin >= segment.begin && begin < segment.end) ||
                (end >= segment.begin && end < segment.end) ||
                (segment.begin >= begin && segment.begin < end) ||
                (segment.end >= begin && segment.end < end); 
        }
    }

}
